/**==============================================================================
            Copyright (c) 2013 Moritz Hader, Marcel Schrder
            All Rights Reserved.

	@file  HeimdallAR.java

	@brief This class implements the base-functionality that is required
	to render objects to an openGL surface and detecting trackables in a
	camera stream. It provides methods to add and remove objects from the
	render-tree. OpenGL access is implemented using native-code. All necessary
	calls are managed within this class. Please note, that the onCreate, initSystem,
	update and destroy methods must be extended for this class to work.
   
   This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

==============================================================================*/

package de.ibm.dhbw.heimdall;

import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.qualcomm.QCAR.QCAR;

import de.ibm.dhbw.R;
import de.ibm.dhbw.heimdall.egl.HeimdallGLView;
import de.ibm.dhbw.heimdall.egl.HeimdallRenderer;
import de.ibm.dhbw.heimdall.engine.Heimdall3DObject;
import de.ibm.dhbw.heimdall.engine.HeimdallAbstractElement;
import de.ibm.dhbw.heimdall.engine.HeimdallNode;
import android.app.Activity;
import android.app.ActionBar.LayoutParams;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;


public class HeimdallAR extends Activity{
	
	public static Logger Log = Logger.getLogger(HeimdallAR.class.getCanonicalName());
	
	//Layout Elements
	private ImageView SplashScreenView;
	private FrameLayout Flayout;
	private HeimdallGLView GLView;
	private HeimdallRenderer GLRenderer;
	
	//Handles and Runnables for asynchronous Tasks (like loading)
	private SplashAsynchronusTask SplashTask;
	private Runnable SplashRunnable;
	private Handler SplashHandler;
	
	//Timing
	private long SplashStartTime = 0;
	private int SplashTargetTime = 3000; //The SplashScreen must at least last this long.
	
	//Local Node with 3D-Objects
	public HeimdallNode root;
	private ArrayList<Heimdall3DObject> natifiedObjects;
	
	//Load the native library into the code
	static{
		HeimdallAR.LoadNativeLib("QCAR");
		HeimdallAR.LoadNativeLib("HeimdallARNative");
	}
	
	//Native Methods
	native void CleanUpEnvironment();
	native void Copy3DObjectsToNative();
	native void LoadQCARTrackables();
	native void LoadScreenDimensions(int width, int height);
	native void PrepareDeviceProjection();
	native void SetupCameraInstance();
	
	@Override
	protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        
        //Define the layout base_layout.xml as the active content-layout
        setContentView(R.layout.base_layout);
        
        Log.log(Level.INFO, "Creating and adding Splash-Screen");
        
        //Create a custom ImageView that displays our splash screen
        this.SplashScreenView = new ImageView(this);
        	this.SplashScreenView.setImageResource(R.drawable.splash_screen_image_targets);
        	
        //Create the GLSurfaceView and the GLRenderer
        this.GLRenderer = new HeimdallRenderer(this);
        this.GLView = new HeimdallGLView(this, true, 0, 16);
        	this.GLView.setRenderer(this.GLRenderer);
        	
        //Receive the handle to the FrameLayout defined in the base_layout
    	this.Flayout = (FrameLayout) findViewById(R.id.outerframe);
    		
    		//Parameters to make the splash-screen match the parent
        	this.attachSplashScreen();
        	
        Log.log(Level.INFO, "Starting asynchronus SplashTask");
        
        //Prepare the root-node
        this.root = new HeimdallNode();
        
        //Setup the Asynchronus Task and execute it
        this.SplashTask = new SplashAsynchronusTask();
        
        	//Before executing the splash-Task, call the create-method
        	this.create();
        
        this.SplashTask.execute();
        
    }
	

	@Override
	protected void onDestroy(){
		super.onDestroy();
		
		Log.info("onDestroy");

		this.destroy();
		//Call the native Method to cleanup
				CleanUpEnvironment();
		//TODO: There must be a workaround for this....
		
		if(this.SplashHandler != null){
			this.SplashHandler.removeCallbacks(this.SplashRunnable);
			this.SplashHandler = null;
			this.SplashRunnable = null;
		}
		
		//Tell the framework to deinitialize itself
		QCAR.deinit();
	}
	
	@Override
	protected void onResume(){
		super.onResume();
		
		Log.info("onResume");
		
		//Let QCAR know, that we can resume our operations
		QCAR.onResume();
		
		this.setScreenOn(true);
		
		if(GLView != null){
			GLView.setVisibility(HeimdallGLView.VISIBLE);
			GLView.onResume();
		}
		
		if(this.GLRenderer != null){
			this.GLRenderer.setRunning(true);
		}
	}
	
	@Override
	protected void onPause(){
		super.onPause();

		
		Log.info("onPause");
		
		if(this.GLRenderer != null){
			this.GLRenderer.setRunning(false);
		}
		
		if(GLView != null){
			GLView.setVisibility(HeimdallGLView.INVISIBLE);
			GLView.onPause();
		}
		
		this.finish();
		
		//if(!isFinishing()){ //?????
			//this.onDestroy();
		//}
		
		//Pause the QCAR-Framework
		QCAR.onPause();
	}
	
	/**
	 * Called when the splash-Screen is to be detached from the FrameLayout.
	 */
	public void detachSplashScreen(){
		if(this.Flayout != null){
			this.Flayout.removeView(SplashScreenView);
		}
	}
	
	/**
	 * Called when the splash-Screen is to be attached from the FrameLayout.
	 */
	public void attachSplashScreen(){
		if(this.Flayout != null){
			LayoutParams parms = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);	
        	this.Flayout.addView(this.SplashScreenView, parms);
		}
	}
	
	/**
	 * Called when the GL-Screen is to be attached from the FrameLayout.
	 */
	public void attachGLView(){
		if(this.Flayout != null){
			LayoutParams parms = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);	
        	this.Flayout.addView(this.GLView, parms);
		}
	}
	
	/**
	 * This method is called during initialization from an asynchronous loader-task.
	 * Note that you may not create task handlers from within this method. Use the create
	 * method instead.
	 */
	public void initSystem(){
		throw new RuntimeException("Error: The initSystem() method must be implemented");
	}
	
	/**
	 * This task is called during the update-routine of the Renderer.
	 * @param tpf
	 */
	public void update(float tpf){
		throw new RuntimeException("Error: The update() method must be implemented");
	}
	
	/**
	 * This method is called after the onCreate method of this activity. Use it to
	 * initialize everything, that is not supposed to be loaded during the splash-screen
	 */
	public void create(){
		throw new RuntimeException("Error: The create() method must be implemented");
	}
	
	/**
	 * This method is called after the onDestroy method of this activity. Use it to
	 * deinitialize everything
	 */
	public void destroy(){
		throw new RuntimeException("Error: The destory() method must be implemented");
	}
	
	
	public void setScreenOn(boolean state){
		if(state){
			this.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
		}else{
			this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
		}
	}
	/*
	 * Subclass: Handles the asynchronous Task for loading the necessary Data and displaying the progress
	 */
	
	public class SplashAsynchronusTask extends AsyncTask<Void, Integer, Boolean>{
		
		public Logger SplashLog = Logger.getLogger(SplashAsynchronusTask.class.getCanonicalName());
		
		private int progressCount;
		
		@Override
		protected Boolean doInBackground(Void... params) {
			
			SplashLog.log(Level.INFO, "Asynchronus Splash-Task was triggered and now starts running in background.");
			
			this.progressCount = 0;
			SplashStartTime = System.currentTimeMillis();
			
			// 1) Set the initialization-Prameters of the QCAR-Framework, so it knows that we are using EGL 2.0 
			QCAR.setInitParameters(HeimdallAR.this, QCAR.GL_20);
			
			
			do{ // 2) Depending on the device, the init-method may take several iterations
				this.progressCount = QCAR.init();
			}while(!isCancelled() && this.progressCount < 100 && this.progressCount >= 0); //The QCAR-Framework may return -1 if an error occured. Stop the loop in that case
			
			
			// 3) Load the required Trackables into the Framework
			LoadQCARTrackables();
			
			
			// 4) Prepare the device-specifig metrics
			DisplayMetrics screenDim = new DisplayMetrics();
				getWindow().getWindowManager().getDefaultDisplay().getMetrics(screenDim);
				
				LoadScreenDimensions(screenDim.widthPixels, screenDim.heightPixels);
			
				
			// 5) Setup the Camera of the QCAR-Framework/the device
			SetupCameraInstance();
			
			// 6) Load the device specific projection-matrix
			PrepareDeviceProjection();
			
			// 7) Call the Init-Method for custom initializations outside the framework
			initSystem();

			
			return true;
		}
		
		@Override
		protected void  onPostExecute(Boolean result){
			if(result){
				SplashLog.log(Level.INFO, "Aynchronus Splash-Task successfully executed. Splash-Screen is now decoupled.");
				
				/*
				 * Create a runnable that calls the detach-method from another Thread than the AsynchTask.
				 * This is due to the fact, that the AsynchTask is not meant to be used for longer periods 
				 * than simple loading while giving feedback to the ui. The runnable can run and sleep as long
				 * as desired without disturbing the rest of the app.
				 */
				SplashRunnable = new SplashScreenRunnable();
				
				/*
				 * We calculate the necessary delay by the difference between the start and the current time.
				 * Then we subtract the TargetTime(desired Splash time) and delay the execution by the resulting time.
				 * If the TimeToSleep is smaller than 0, we run the Runnable right away.
				 */
				long TimeToSleep = System.currentTimeMillis()-SplashStartTime;
					TimeToSleep =  SplashTargetTime - TimeToSleep;
					
				SplashHandler = new Handler();
					if(TimeToSleep <= 0){
						SplashHandler.post(SplashRunnable);
					}else{
						SplashHandler.postDelayed(SplashRunnable, TimeToSleep);
					}
			}
		}

	}
	
	public class SplashScreenRunnable implements Runnable{
		@Override
		public void run() {
			//TODO:Start Rendering and Stuff
			detachSplashScreen();
			
			//Activate the renderer
			GLRenderer.setRunning(true);
			attachGLView();
			
		}
	}
	
	public static boolean LoadNativeLib(String library)
    {
        try
        {
        	Log.log(Level.INFO, "Loading native library "+library);
        	
            System.loadLibrary(library);
            
            return true;
        }
        catch (SecurityException secex)
        {
        	Log.log(Level.SEVERE, "Error while loading the native library "+library,secex);
        }
        catch (UnsatisfiedLinkError ulinkerr)
        {
            Log.log(Level.SEVERE, "Error while loading the native library "+library,ulinkerr);
        }
        
        return false;
    } 
	
	/**
	 * Adds the given Heimdall3DObject to the root-node and returns the index it was added at. In case of error -1 is returned.
	 * @param obj - Heimdall3DObject
	 * @return int
	 */
	public void attachObject(HeimdallAbstractElement obj){
		this.root.attachElement(obj);
		
		obj.natifyObject();
	}
	
	public void detachObject(HeimdallAbstractElement obj){
		this.root.detachElement(obj);
		
		obj.denatifyObject();
	}
	
	
	
	//Below are the methods concerned with the transfer of 3D-object-Data from JVM to Native
	
	/**
	 * Take the local node transform the tree structure into a List, so it can be processed by the native code.
	 */
	/*public void natify(){
		
		this.natifiedObjects = new ArrayList<Heimdall3DObject>();
		
		//Do a depth first search throughout the object-node. Ascend at max 8 Levels
		search3DElement(this.natifiedObjects, this.root, 8);
		
		//Call the method to make this shit native
		Copy3DObjectsToNative();
		
		/*for(Heimdall3DObject obj: this.natifiedObjects){
			Log.info("Native Key: "+obj.getIdentifier());
		}
		
	}*//*
	
		private void search3DElement(ArrayList<Heimdall3DObject> list, HeimdallNode currentNode, int maxDepth){
			
			for(HeimdallAbstractElement elem: currentNode.getLocNode()){
				

				//the element must be a 3DObject, apply the current Node properties and store it onto the list(not implemented yet)
				//elem.inheritProperties(currentNode);
				
				//If the element is an instance of Node, we'll have to descend further into the tree
				if((elem instanceof HeimdallNode) == true){
					
					//Only descend, if the maxdepth was not reached yet
					if(maxDepth > 0){
						search3DElement(list, (HeimdallNode)elem, maxDepth-1);
					}
					
					continue;
				}
				
				list.add((Heimdall3DObject)elem);
				
			}
		}*/
	
	/*public Heimdall3DObject getNatifiedObject(int index){
		return this.natifiedObjects.get(index);
	}
	
	public int getNatifiedObjectsLength(){
		return this.natifiedObjects.size();
	}*/
	
	/**
	 * return the collection that contains the currently detected marker IDs
	 * @return int[]
	 */
	public int[] getMarkerIDs(){
		
		return this.GLRenderer.getcurrentTransMat();
	}
	
}
